package com.example.shop.goods.service.impl;

import com.example.shop.common.api.ServiceReturn;
import com.example.shop.common.api.ServiceStateCode;
import com.example.shop.common.bo.Page;
import com.example.shop.common.util.CachedThreadPoolUtil;
import com.example.shop.es.dto.CustomHighlightQuery;
import com.example.shop.es.dto.ElasticSearchQueryParam;
import com.example.shop.es.service.ElasticSearchClient;
import com.example.shop.goods.dao.GoodsBrowseInfoDao;
import com.example.shop.goods.dao.GoodsDetailBrowseInfoDao;
import com.example.shop.goods.dao.MerchantGoodsDetailBrowseInfoDao;
import com.example.shop.goods.dto.*;
import com.example.shop.goods.enums.GoodsSortRules;
import com.example.shop.goods.enums.HighLightSetup;
import com.example.shop.goods.enums.ShelfState;
import com.example.shop.goods.enums.SortOrder;
import com.example.shop.goods.service.BrowseGoodsService;
import com.example.shop.pojo.GoodsType;
import com.example.shop.pojo.Merchant;
import com.example.shop.rabbitmq.component.GoodsComponent;
import com.example.shop.rabbitmq.dto.InventoryDetect;
import com.example.shop.redis.enums.RedisKey;
import com.example.shop.redis.service.LettuceClient;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import java.io.*;
import java.util.*;

@Service
public class BrowseGoodsServiceImpl implements BrowseGoodsService {

    private final ElasticSearchClient esClient;
    private final GoodsDetailBrowseInfoDao detailBrowseInfoDao;
    private final GoodsBrowseInfoDao browseInfoDao;
    private final ObjectMapper esObjectMapper;
    private final LettuceClient redisClient;
    private final RabbitTemplate rabbitTemplate;
    private final MerchantGoodsDetailBrowseInfoDao merchantGoodsDetailBrowseInfoDao;
    public BrowseGoodsServiceImpl(ElasticSearchClient esClient, GoodsDetailBrowseInfoDao detailBrowseInfoDao, GoodsBrowseInfoDao browseInfoDao, @Qualifier("esObjectMapper") ObjectMapper esObjectMapper, LettuceClient redisClient, RabbitTemplate rabbitTemplate, MerchantGoodsDetailBrowseInfoDao merchantGoodsDetailBrowseInfoDao) {
        this.esClient = esClient;
        this.detailBrowseInfoDao = detailBrowseInfoDao;
        this.browseInfoDao = browseInfoDao;
        this.esObjectMapper = esObjectMapper;
        this.redisClient = redisClient;
        this.rabbitTemplate = rabbitTemplate;
        this.merchantGoodsDetailBrowseInfoDao = merchantGoodsDetailBrowseInfoDao;
    }

//    @PostConstruct
//    public void init(){
//        esClient.bulkSave(detailBrowseInfoDao.selectPage(10,pageSize), WriteRequest.RefreshPolicy.NONE);
//    }


    /**
     * 将GoodsQueryParam查询参数转换为es查询参数ElasticSearchQueryParam
     * @param queryParam 查询参数
     * @param fondRatioList 爱好推荐
     * @return 返回es查询参数列表
     */
    private List<ElasticSearchQueryParam> toElasticSearchQueryParam(GoodsQueryParam queryParam,Set<GoodsTypeFondRatio> fondRatioList){

        //所有爱好条件都要满足的 --- 商品查询条件
        BoolQueryBuilder goodsQueryParamBuilder = QueryBuilders.boolQuery();
        if(queryParam.getMerchantId()!=null){
            goodsQueryParamBuilder.must(QueryBuilders.termQuery("merchantId",queryParam.getMerchantId()));
        }
        if(queryParam.getGoodsId()!=null){
            goodsQueryParamBuilder.must(QueryBuilders.termQuery("goodsId",queryParam.getGoodsId()));
        }
        if(queryParam.getGoodsType()!=null){
            goodsQueryParamBuilder.must(QueryBuilders.termQuery("goodsType.keyword",queryParam.getGoodsType().name()));
        }
        if(queryParam.getGoodsName()!=null){
            goodsQueryParamBuilder.must(QueryBuilders.termQuery("goodsName.keyword",queryParam.getGoodsName()));
        }
        if(queryParam.getFuzzyGoodsName()!=null){
            goodsQueryParamBuilder.must(QueryBuilders.matchQuery("goodsName",queryParam.getFuzzyGoodsName()));
        }
        if(queryParam.getShelfState()!=null){
            goodsQueryParamBuilder.must(QueryBuilders.termQuery("shelfState.keyword", ShelfState.getShelfState(queryParam.getShelfState())));
        }
        //所有爱好条件都要满足的 --- 范围查询条件
        List<RangeQueryParam> rangeQueryParamList = queryParam.getRangeQueryParams();
        if(rangeQueryParamList!=null){
            for(RangeQueryParam rangeQueryParam:rangeQueryParamList){
                RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(rangeQueryParam.getFieldType().getEsField());
                if(rangeQueryParam.isLowerLimitInclude()){
                    rangeQueryBuilder.gte(rangeQueryParam.getLowerLimit());
                }else{
                    rangeQueryBuilder.gt(rangeQueryParam.getLowerLimit());
                }
                if(rangeQueryParam.isUpperLimitInclude()){
                    rangeQueryBuilder.lte(rangeQueryParam.getUpperLimit());
                }else{
                    rangeQueryBuilder.lt(rangeQueryParam.getUpperLimit());
                }
                goodsQueryParamBuilder.must(rangeQueryBuilder);
            }
        }
        //所有爱好条件都要满足的 --- 排序规则
        Sort sort;
        Sort.Direction direction = Sort.Direction.valueOf(queryParam.getSortOrder().name());
        if(queryParam.getSortRules()!=null){
            sort = Sort.by(direction,queryParam.getSortRules().getEsField(), GoodsSortRules.RECOMMEND_SCORE.getEsField());
        }else{
            sort = Sort.by(direction,GoodsSortRules.RECOMMEND_SCORE.getEsField());
        }

        //高亮处理
        HighlightBuilder highlightBuilder = null;
        //如果有高亮设置，则使用自定义的，否则使用默认的
        HighLightParam highLight = queryParam.getHighLight();
        if(highLight != null && highLight.getHighLightSetup() != null){
            highlightBuilder = new HighlightBuilder();
            //如果没有设置高亮标签，则使用默认的
            //设置高亮前缀
            highlightBuilder.preTags(highLight.getPreTag() != null ? CustomHighlightQuery.preTag : highLight.getPreTag())
                    //设置高亮后缀
                    .postTags(highLight.getPostTag() != null ? CustomHighlightQuery.postTag : highLight.getPostTag());
            HighLightSetup highLightSetup = highLight.getHighLightSetup();
            switch (highLightSetup){
                case NONE: break;
                case GOODS_NAME:
                    //设置高亮的字段，必须是字符串，而且还在查询出来的结果里有的
                    //如果是text，则高亮分词结果的，而不是一整个值
                    highlightBuilder.field("goodsName.keyword");
                    //设置高亮的字段，必须是字符串，而且还在查询出来的结果里有的
                    //如果是text，则高亮分词结果的，而不是一整个值
                    highlightBuilder.field("goodsName");
                    break;
            }
        }


        List<ElasticSearchQueryParam> searchQueryParams = new ArrayList<>();
        Page page = queryParam.getPage();
        if(fondRatioList!=null && !fondRatioList.isEmpty()){
            for(GoodsTypeFondRatio fond:fondRatioList){
                BoolQueryBuilder queryFondTypeBuilder = QueryBuilders.boolQuery()
                        .must(QueryBuilders.termQuery("goodsType.keyword", fond.getGoodsType().name()))
                        //爱好查询要求满足的其他条件
                        .must(goodsQueryParamBuilder);
                //设置条件查询
                ElasticSearchQueryParam.Builder searchQueryParamBuilder = ElasticSearchQueryParam.builder(queryFondTypeBuilder)
                        //设置分页情况
                        .setPageable(PageRequest.of(page.getPageIndex(), fond.getNum()))
                        //设置排序规则
                        .setSort(sort)
                        //设置高亮
                        .setHighlight(highlightBuilder);
                searchQueryParams.add(searchQueryParamBuilder.build());
            }
        }else{
            BoolQueryBuilder queryFondTypeBuilder = QueryBuilders.boolQuery()
                    //爱好查询要求满足的其他条件
                    .must(goodsQueryParamBuilder);
            //设置条件查询
            ElasticSearchQueryParam.Builder searchQueryParamBuilder = ElasticSearchQueryParam.builder(queryFondTypeBuilder)
                    //设置分页情况
                    .setPageable(PageRequest.of(page.getPageSize() * (page.getPageIndex() - 1), page.getPageSize()))
                    //设置排序规则
                    .setSort(sort)
                    //设置高亮
                    .setHighlight(highlightBuilder);
            searchQueryParams.add(searchQueryParamBuilder.build());
        }

        return searchQueryParams;
    }


    @Override
    public ServiceReturn<MerchantGoodsDetailBrowseInfo> merchantObtainDetailInfo(Merchant merchant, long goodsId) {
        MerchantGoodsDetailBrowseInfo detailBrowseInfo = merchantGoodsDetailBrowseInfoDao.selectOne(merchant.getMerchantId(),goodsId);
        if(detailBrowseInfo!=null){
            return new ServiceReturn<>(ServiceStateCode.SUCCESS,detailBrowseInfo);
        }
        return new ServiceReturn<>(ServiceStateCode.NOT_EXIST_GOODS,null);
    }

    @Override
    public ServiceReturn<GoodsDetailBrowseInfo> obtainDetailInfo(long id) {
        //从es里查询该id的商品详细信息
        GoodsDetailBrowseInfo info = esClient.getDocumentMustClass(id, GoodsDetailBrowseInfo.class);
        if(info==null){
            //如果es拿不到该数据，则到数据库中拿
            info = detailBrowseInfoDao.selectOne(id);
            if(info==null){
                CachedThreadPoolUtil.submit(()->esClient.saveDocument(new GoodsDetailBrowseInfo(id)));
                return new ServiceReturn<>(ServiceStateCode.NO_SUCH_RECORD,null);
            }else{
                GoodsDetailBrowseInfo finalInfo = info;
                CachedThreadPoolUtil.submit(()->esClient.saveDocument(finalInfo));
            }
        }
        //由于商品的库存数据实时性可能在redis数据库里
        Map<Object, Object> inventoryMap = redisClient.hGetMap(RedisKey.HashMap.GOODS_INVENTORY.getKey() + id);
        //如果该商品在redis数据库里有进行库存操作，则inventoryMap不为空，即reids里的库存是最新库存数据。
        if(!inventoryMap.isEmpty()){
            List<GoodsStyle> goodsStyleList = info.getGoodsStyleList();
            //商品的库存数据更换为redis数据库里的库存数据
            for(GoodsStyle style:goodsStyleList){
                style.setClassInventory((Integer) inventoryMap.get(style.getGoodsClass()));
            }
        }else{
            //将浏览的款式的库存数据保存到redis数据库里
            styleSaveInRedis(info.getGoodsId(),info.getGoodsStyleList());
        }

        return new ServiceReturn<>(ServiceStateCode.SUCCESS,info);
    }


    /**
     * 将该款式的库存量保存到redis中
     * @param goodsId 商品id
     * @param goodsStyleList 该商品的款式列表
     */
    private synchronized void styleSaveInRedis(long goodsId,  List<GoodsStyle> goodsStyleList){
        boolean exist = redisClient.existKey(RedisKey.HashMap.GOODS_INVENTORY.getKey() + goodsId);
        //如果该商品款式不在redis中，则将其缓存到redis中
        if(!exist){
            HashMap<String, Integer> map = new HashMap<>();
            for(GoodsStyle goodsStyle:goodsStyleList){
                map.put(goodsStyle.getGoodsClass(),goodsStyle.getClassInventory());
            }

            /**
             * 将该商品的款式延迟发送到mq队列中，隔一段时间后会检测redis中该商品的款式的库存是否太久未使用
             * 如果太久未使用，则删除该key，更新到数据库和es中，进行数据同步，并重新发送新库存的消息
             * 如果在规定时间内有使用，则重新发布消息(最新的款式信息)入队，等待下一次检测
             * 如果检测redis中该商品的库存map不存在，则消费该消息，不进行处理
             */
            //发送消息
            rabbitTemplate.convertAndSend(GoodsComponent.GOODS_EXCHANGE,GoodsComponent.INVENTORY_DETECT_ROUTING_KEY,
                    new InventoryDetect(goodsId,map));

            //保存到redis中
            redisClient.hSetMap(RedisKey.HashMap.GOODS_INVENTORY.getKey() + goodsId,map);
        }
    }

    @Override
    public ServiceReturn<List<GoodsBrowseInfo>> obtainBrowseList(GoodsQueryParam queryParam, Set<GoodsTypeFondRatio> fondRatioList) {
        if(queryParam ==null){
            queryParam =GoodsQueryParam.builder()
                    //按分值排序
                    .setGoodsSortRules(GoodsSortRules.RECOMMEND_SCORE)
                    //降序
                    .setSortOrder(SortOrder.DESC)
                    //分页
                    .setPage(new Page(1))
                    .build();
        }
        if(queryParam.getPage()==null){
            try {
                throw new Exception("分页不能为null");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        List<GoodsBrowseInfo> searchResult = esClient.multiSearch(toElasticSearchQueryParam(queryParam,fondRatioList),GoodsBrowseInfo.class);
        if(searchResult.isEmpty() || searchResult.size()<queryParam.getPage().getPageSize()){
            searchResult = browseInfoDao.fondSelect(queryParam,fondRatioList);
            //复制一份该搜索结果集，保存到es中
            try {
                String json = esObjectMapper.writeValueAsString(searchResult);
                //异步保存searchResult
                CachedThreadPoolUtil.submit(()->{
                    try {
                        List<GoodsBrowseInfo> searchResultCopy = esObjectMapper.readValue(json,new TypeReference<List<GoodsBrowseInfo>>(){});
                        if(searchResultCopy!=null){
                            esClient.bulkSave(searchResultCopy, WriteRequest.RefreshPolicy.NONE);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
                //高亮处理，由于mysql查询出来是没有高亮的，所以要进行高亮，但是es查询出来是有高亮的，则就不需要进行高亮
                HighLightParam highLightParam = queryParam.getHighLight();
                if(highLightParam!=null && highLightParam.getHighLightSetup()!=null){
                    //如果没有设置高亮的标签，则使用默认的标签
                    String preTag = highLightParam.getPreTag() != null ? highLightParam.getPreTag() : CustomHighlightQuery.preTag;
                    String postTag = highLightParam.getPostTag() != null ? highLightParam.getPostTag() : CustomHighlightQuery.postTag;
                    String fuzzyGoodsName = queryParam.getFuzzyGoodsName();
                    if(queryParam.getGoodsName()!=null){
                        for (GoodsBrowseInfo info : searchResult) {
                            if (!info.getGoodsName().contains(postTag)) {
                                info.setGoodsName(preTag + info.getGoodsName() + postTag);
                            }
                        }
                    }else if(fuzzyGoodsName!=null){
                        for (GoodsBrowseInfo info : searchResult) {
                            if(!info.getGoodsName().contains(postTag)){
                                String highLight = info.getGoodsName().replaceAll(fuzzyGoodsName,preTag + fuzzyGoodsName + postTag);
                                info.setGoodsName(highLight);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("Goods模块的BrowseGoodsServiceImpl的obtainBrowseList出错");
            }

        }
        return new ServiceReturn<>(ServiceStateCode.SUCCESS,searchResult);
    }

    @Override
    public ServiceReturn<List<GoodsBrowseInfo>> obtainBrowseListByType(GoodsType goodsType,int pageIndex) {
        GoodsQueryParam.Builder builder = GoodsQueryParam.builder();
        builder.setGoodsType(goodsType)
                .setPage(new Page(pageIndex,Page.DEFAULT_PAGE_SIZE));
        return obtainBrowseList(builder.build(),null);
    }

    @Override
    public ServiceReturn<List<GoodsBrowseInfo>> obtainBrowseListLikeName(String name, Set<GoodsTypeFondRatio> fondRatioList,int pageIndex) {
        GoodsQueryParam.Builder builder = GoodsQueryParam.builder();
        builder.setFuzzyGoodsName(name)
                .setPage(new Page(pageIndex,Page.DEFAULT_PAGE_SIZE));
        return obtainBrowseList(builder.build(),fondRatioList);
    }

    @Override
    public ServiceReturn<List<GoodsBrowseInfo>> obtainBrowseListByRange(RangeQueryParam rangeQueryParam, Set<GoodsTypeFondRatio> fondRatioList, int pageIndex) {
        GoodsQueryParam.Builder builder = GoodsQueryParam.builder();
        builder.addRangeQueryParam(rangeQueryParam)
                .setPage(new Page(pageIndex,Page.DEFAULT_PAGE_SIZE));
        return obtainBrowseList(builder.build(),fondRatioList);
    }

    @Override
    public ServiceReturn<List<GoodsBrowseInfo>> merchantGoodsList(Long merchantId,Set<GoodsTypeFondRatio> fondRatioList,int pageIndex) {
        GoodsQueryParam.Builder builder = GoodsQueryParam.builder();
        builder.setMerchantId(merchantId)
                .setPage(new Page(pageIndex,Page.DEFAULT_PAGE_SIZE));
        return obtainBrowseList(builder.build(),fondRatioList);
    }

}
